﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.IO;
using System.Collections;

namespace Kokomo.Mirror.CliMetadata.DebugInfo
{
	public class DebugDatabaseBuilder
	{
		public DebugDatabaseBuilder(
			MethodInfo userEntryPoint
			)
		{
			this._userEntryPoint = userEntryPoint;
			this._documents = new List<SymDocumentBuilder>();
			this._stringHeap = new MemoryStream();
			this._miscHeap = new MemoryStream();
			this._stringTable = new Dictionary<string, int>();
		}

		private MethodInfo _userEntryPoint;
		private List<SymDocumentBuilder> _documents;
		private MemoryStream _stringHeap;
		private MemoryStream _miscHeap;
		private Dictionary<string, int> _stringTable;

		private int GetString(string str)
		{
			int index;
			if (this._stringTable.TryGetValue(str, out index)) return index;

			str += '\0';
			byte[] stringBytes = Encoding.UTF8.GetBytes(str.ToCharArray());
			index = (int)_stringHeap.Position;
			this._stringHeap.Write(stringBytes, 0, stringBytes.Length);

			this._stringTable[str] = index;

			return index;
		}
		private int WriteMiscBlob(byte[] data)
		{
			int index = (int)this._miscHeap.Position;
			this._miscHeap.Write(data, 0, data.Length);
			return index;
		}

		public SymDocumentBuilder DefineDocument()
		{
			SymDocumentBuilder docBuilder = new SymDocumentBuilder();
			this._documents.Add(docBuilder);

			return docBuilder;
		}

		private void WriteScope(
			DebugDatabase database,
			SymMethodScopeBuilder scopeBuilder,
			int parentScopeNumber
			)
		{
			int thisScopeIndex = database.SymScopeRows.Count;

			SymScope scope = new SymScope()
			{
				Parent = parentScopeNumber,
				StartOffset = scopeBuilder.StartOffset,
				EndOffset = scopeBuilder.StartOffset + scopeBuilder.Length,
				HasChildren = (scopeBuilder.scopes.Count > 0),
				HasVars = (scopeBuilder.variables.Count > 0)
			};
			database.SymScopeRows.Add(scope);

			foreach (var variableBuilder in scopeBuilder.variables.Values)
			{
				SymVariable variable = new SymVariable()
				{
					Scope = thisScopeIndex,
					Name = this.GetString(variableBuilder.name),
					Signature = this.WriteMiscBlob(variableBuilder.signature),
					SignatureSize = variableBuilder.signature.Length,
					AddressKind = 1,
					Address1 = variableBuilder.localIndex,
					StartOffset = scope.StartOffset,
					EndOffset = scope.StartOffset + scope.EndOffset,
				};
				database.SymVariableRows.Add(variable);
			}
			foreach (var importBuilder in scopeBuilder.namespaceImports)
			{
				SymUsing import = new SymUsing()
				{
					Scope = thisScopeIndex,
					Namespace = this.GetString(importBuilder)
				};
				database.SymUsingRows.Add(import);
			}
			foreach (var childScopeBuilder in scopeBuilder.scopes)
			{
				this.WriteScope(database, childScopeBuilder, thisScopeIndex);
			}
		}

		public DebugDatabase Save(string fileName)
		{
			if (fileName == null) throw new ArgumentNullException("fileName");

			DebugDatabase database = new DebugDatabase(fileName);
			foreach (var documentBuilder in this._documents)
			{
				SymDocument document = new SymDocument()
				{
					Language = documentBuilder.Language,
					LanguageVendor = documentBuilder.LanguageVendor,
					DocumentType = documentBuilder.DocumentType,
					AlgorithmId = documentBuilder.AlgorithmId,
					UrlEntry = this.GetString(documentBuilder.Url)
				};

				int documentIndex = database.SymDocumentRows.Count;
				database.SymDocumentRows.Add(document);

				foreach (var methodBuilder in documentBuilder.methods.Values)
				{
					SymMethod method = new SymMethod()
					{
						MethodToken = methodBuilder.Token,
						Documents = new Range(documentIndex, documentIndex + 1)
					};

					int startScopeIndex = database.SymScopeRows.Count;
					int startUsingIndex = database.SymUsingRows.Count;
					int startSequencePointIndex = database.SymSequencePointRows.Count;
					int startVarIndex = database.SymVariableRows.Count;

					this.WriteScope(database, methodBuilder.RootScope, -1);

					foreach (var sequencePointBuilder in methodBuilder.sequencePoints.Values)
					{
						SymSequencePoint sequencePoint = new SymSequencePoint()
						{
							Offset = sequencePointBuilder.CilOffset,
							StartLine = sequencePointBuilder.SourceStartLine,
							StartColumn = sequencePointBuilder.SourceStartColumn,
							EndLine = sequencePointBuilder.SourceEndLine,
							EndColumn = sequencePointBuilder.SourceEndColumn
						};
						database.SymSequencePointRows.Add(sequencePoint);
					}

					method.Scopes = new Range(startScopeIndex, database.SymScopeRows.Count);
					method.Vars = new Range(startVarIndex, database.SymVariableRows.Count);
					method.Using = new Range(startUsingIndex, database.SymUsingRows.Count);
					method.SequencePoints = new Range(startSequencePointIndex, database.SymSequencePointRows.Count);
					database.SymMethodRows.Add(method);
				}
			}

			DebugDatabaseHeader header = new DebugDatabaseHeader()
			{
				Signature = DebugDatabaseHeader.CilDbSignature,
				Guid = DebugDatabaseHeader.CliDbGuid,
				CountOfMethods = database.SymMethodRows.Count,
				CountOfScopes = database.SymScopeRows.Count,
				CountOfVars = database.SymVariableRows.Count,
				CountOfUsing = database.SymUsingRows.Count,
				CountOfConstants = database.SymConstantRows.Count,
				CountOfDocuments = database.SymDocumentRows.Count,
				CountOfSequencePoints = database.SymSequencePointRows.Count,
				CountOfMiscBytes = (int)this._miscHeap.Length,
				CountOfStringBytes = (int)this._stringHeap.Length
			};
			if (this._userEntryPoint != null)
				header.UserEntryPoint = this._userEntryPoint.MetadataToken;

			using (FileStream stream = new FileStream(fileName, FileMode.Create))
			{
				stream.WriteStruct(header);

				IEnumerable<IEnumerable> tables = database.GetTables();
				foreach (var table in tables)
				{
					foreach (var row in table)
					{
						stream.WriteStruct(row);
					}
				}
				this._miscHeap.CopyTo(stream);
				this._stringHeap.CopyTo(stream);
			}

			return database;
		}
	}
}
